package hk.ust.comp3111.ustnutrition.data;

import hk.ust.comp3111.ustnutrition.MiscUtils;
import hk.ust.comp3111.ustnutrition.data.db.TableStatus;
import hk.ust.comp3111.ustnutrition.data.db.User;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.couchbase.lite.*;
import com.couchbase.lite.replicator.*;

import android.util.Log;

public class DataTable<T extends DataRow> extends DataTableQuery<T>{

	static final String TAG = "hk.ust.comp3111.ustnutrition.data.DataTable";

	Map<String, T> rows = new HashMap<String, T>();
	Map<String, View> views = new HashMap<String, View>();
	DataAccessor accessor;
	Class<T> tableClass;
	Class<T> rowClass;
	Database database;
	URL syncUrl;
	boolean canWrite, isPublic;

	Replication pullReplication;
	Replication pushReplication;

	public DataTable(DataAccessor accessor, Class<T> tableClass,
			Class<T> rowClass, URL syncUrl) {
		this.parent = this;
		this.accessor = accessor;
		this.tableClass = tableClass;
		this.rowClass = rowClass;
		this.syncUrl = syncUrl;

		createDatabase();
	}

	public void resetDatabase() {
		try {
			this.setQuery(null);
			if (database != null) {
				database.delete();
				database = null;
			}
			createDatabase();
		} catch (CouchbaseLiteException e) {
			Log.e(TAG, "Cannot delete database");
		}
	}

	private void createDatabase() {
		DataTableName name = tableClass.getAnnotation(DataTableName.class);

		try {
			this.canWrite = name.canWrite();
			this.isPublic = name.isPublic();
			
			database = accessor.getManager().getDatabase(name.value());
			database.addChangeListener(this);
			this.setQuery(database.createAllDocumentsQuery());
			sync();
		} catch (CouchbaseLiteException e) {
			Log.e(TAG, "Cannot get database");
		}
	}

	public void sync() {
		stopSync();
		if (this.syncUrl != null && !accessor.getOfflineMode()) {
			User user = DataAccessor.Instance.getUser();
			String itsc = user.getITSC();
			URL mUrl = this.syncUrl;
			boolean logon = user.logon() && itsc != null;
			boolean canWrite = this.canWrite && logon;
			
			if(!logon && !isPublic)
				return;
			
			if(logon && this.canWrite) {
				String userInfo = itsc + ":" + user.getToken();
				try {
					mUrl = new URL(syncUrl.getProtocol() + "://" + userInfo + "@" + syncUrl.getHost() + ":" + syncUrl.getPort() + syncUrl.getPath());
				} catch (MalformedURLException e) {
					throw new RuntimeException(e);
				}
			}
			
			if (canWrite) {
				TableStatus status = DataAccessor.Instance.getConfigs().get(database.getName(), TableStatus.class, true);
				
				if(!itsc.equals(status.getChannel())) {
					for(T item : this) {
						if(!item.isPublic())
							item.setChannel(itsc);
					}
					status.setChannel(itsc);
				}
				
				database.setFilter("channelFilter", new ReplicationFilter() {
				    @SuppressWarnings("unchecked")
					@Override
				    public boolean filter(SavedRevision revision, Map<String, Object> params) {
				    	List<Object> channels = (List<Object>) revision.getProperties().get("channels");
				        if(channels == null || channels.size() != 1) return false;
				        if(channels.get(0).toString().equals(DataAccessor.Instance.getUser().getITSC()))
				        	return true;
				        return false;
				    }
				});
				
				pushReplication = database.createPushReplication(mUrl);
				pushReplication.setFilter("channelFilter");
				pushReplication.setContinuous(true);
				pushReplication.start();
			}

			pullReplication = database.createPullReplication(mUrl);
			pullReplication.setContinuous(canWrite);
			pullReplication.start();
		}
	}
	
	public void stopSync() {
		if(pushReplication != null) {
			pushReplication.stop();
			pushReplication = null;
		}
		if(pullReplication != null) {
			pullReplication.stop();
			pullReplication = null;
		}
		if(this.syncUrl != null && !accessor.getOfflineMode() && canWrite) {
			TableStatus status = DataAccessor.Instance.getConfigs().get(database.getName(), TableStatus.class, true);
			
			User user = DataAccessor.Instance.getUser();
			String itsc = user.getITSC();
			
			if(status.getChannel() != null && itsc == null) {
				for(T item : MiscUtils.iteratorToList(iterator())) {
					if(!item.isNonPublic())
						item.delete();
				}
				status.setChannel(null);
			}
		}
	}

	public Database getDatabase() {
		return database;
	}

	public T get(String key) {
		return get(key, rowClass, false);
	}

	@SuppressWarnings("unchecked")
	public <TNew extends T> TNew get(String key, Class<TNew> mFieldClass,
			boolean createNew) {
		if (key == null)
			return null;
		if (rows.containsKey(key))
			return (TNew) rows.get(key);
		Document doc = database.getExistingDocument(key);
		if (doc == null && createNew) {
			try {
				TNew object = mFieldClass.newInstance();
				return add(object, key) ? object : null;
			} catch (InstantiationException e) {
				Log.e(TAG, e.toString());
			} catch (IllegalAccessException e) {
				Log.e(TAG, e.toString());
			}
		}
		if (doc == null || doc.getProperty("_deleted") != null)
			return null;
		try {
			TNew t = mFieldClass.newInstance();
			t.setTable(this, doc);
			t.refresh();
			rows.put(t.getId(), t);
			return t;
		} catch (InstantiationException e) {
			Log.e(TAG, e.toString());
		} catch (IllegalAccessException e) {
			Log.e(TAG, e.toString());
		}
		return null;
	}

	private View getView(String key) {
		View view;
		if ((view = (View) views.get(key)) == null) {
			DataTableView mview = null;

			DataTableView tview = tableClass.getAnnotation(DataTableView.class);
			if (tview != null && tview.value() == key)
				mview = tview;

			if (mview == null) {
				DataTableViews views = tableClass
						.getAnnotation(DataTableViews.class);
				if (views != null) {
					for (DataTableView cur : views.value())
						if (cur.value() == key) {
							mview = cur;
							break;
						}
				}
			}

			if (mview == null)
				throw new RuntimeException("Definition of view \"" + key
						+ "\" not found");

			Class<? extends DataTableViewField>[] fields = mview.fields();
			final DataTableViewField[] mfields = new DataTableViewField[fields.length];
			for (int i = 0; i < fields.length; i++)
				try {
					mfields[i] = fields[i].newInstance();
				} catch (InstantiationException e) {
					throw new RuntimeException("Cannot create " + fields[i].getName());
				} catch (IllegalAccessException e) {
					throw new RuntimeException("Cannot create " + fields[i].getName());
				}

			view = database.getView(String.format("%s/%s", database.getName(), key));
			view.setMap(new Mapper() {
				@Override
				public void map(Map<String, Object> document, Emitter emitter) {
					List<Object> compoundKey = new ArrayList<Object>();
					for (int i = 0; i < mfields.length; i++)
						compoundKey.add(mfields[i].getValue(document));
					emitter.emit(compoundKey, document);
				}
			}, "1.0");
		}
		return view;
	}

	public DataTableQuery<T> getQuery(String key) {
		return new DataTableQuery<T>(getView(key).createQuery(), this);
	}

	@Override
	public boolean add(T object) {
		return add(object, null);
	}

	public boolean add(T object, String id) {
		if (object.getId() != null)
			throw new IllegalStateException("Object is already in a table");
		Document doc = id == null ? database.createDocument() : database
				.getDocument(id);
		object.setTable(this, doc);
		object.commit();
		rows.put(object.getId(), object);
		return true;
	}

	@Override
	public boolean addAll(Collection<? extends T> arg0) {
		for (T item : arg0)
			add(item);
		return true;
	}

	@Override
	public void clear() {
		for (T item : this)
			remove(item);
	}

	@Override
	public boolean contains(Object object) {
		if (object instanceof DataRow) {
			DataRow field = (DataRow) object;
			if (field.getTable() == this)
				return true;
		}
		return false;
	}

	@Override
	public boolean containsAll(Collection<?> arg0) {
		for (Object item : arg0)
			if (!contains(item))
				return false;
		return true;
	}

	@Override
	public boolean isEmpty() {
		return size() == 0;
	}

	@Override
	public boolean remove(Object object) {
		if (object instanceof DataRow) {
			DataRow field = (DataRow) object;
			if (field.getTable() == this) {
				field.delete();
				return true;
			}
		}
		return false;
	}

	@Override
	public boolean removeAll(Collection<?> arg0) {
		for (Object item : arg0)
			if (!remove(item))
				return false;
		return true;
	}

	public void removeCache(String key) {
		rows.remove(key);
	}

	public void flushCache() {
		rows.clear();
	}

	@Override
	public boolean retainAll(Collection<?> arg0) {
		throw new UnsupportedOperationException();
	}

	@Override
	public int size() {
		return database.getDocumentCount();
	}

}
